/**
 * Copyright (C) 2010-2016 eBusiness Information, Excilys Group
 * Copyright (C) 2016-2020 the AndroidAnnotations project
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed To in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.ohosannotations.internal.core.handler;

import com.helger.jcodemodel.AbstractJClass;
import com.helger.jcodemodel.AbstractJType;
import com.helger.jcodemodel.IJExpression;
import com.helger.jcodemodel.JBlock;
import com.helger.jcodemodel.JConditional;
import com.helger.jcodemodel.JExpr;
import com.helger.jcodemodel.JFieldRef;
import com.helger.jcodemodel.JInvocation;
import com.helger.jcodemodel.JMethod;
import com.helger.jcodemodel.JTryBlock;
import com.helger.jcodemodel.JVar;

import org.ohosannotations.ElementValidation;
import org.ohosannotations.OhosAnnotationsEnvironment;
import org.ohosannotations.Option;
import org.ohosannotations.annotations.Trace;
import org.ohosannotations.handler.BaseAnnotationHandler;
import org.ohosannotations.holder.EComponentHolder;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;

import static com.helger.jcodemodel.JExpr._new;
import static org.ohosannotations.helper.LogHelper.trimLogTag;
import static org.ohosannotations.helper.ModelConstants.generationSuffix;
import static org.ohosannotations.helper.OhosConstants.LOG_DEBUG;
import static org.ohosannotations.helper.OhosConstants.LOG_ERROR;
import static org.ohosannotations.helper.OhosConstants.LOG_FATAL;
import static org.ohosannotations.helper.OhosConstants.LOG_INFO;
import static org.ohosannotations.helper.OhosConstants.LOG_WARN;

/**
 * 跟踪处理程序
 *
 * @author dev
 * @since 2021-07-22
 */
public class TraceHandler extends BaseAnnotationHandler<EComponentHolder> {
    /**
     * 选择跟踪
     */
    public static final Option OPTION_TRACE = new Option("trace", "false");

    private static final int DOMAIN = 0x99999;

    /**
     * 跟踪处理程序
     *
     * @param environment 环境
     */
    public TraceHandler(OhosAnnotationsEnvironment environment) {
        super(Trace.class, environment);
    }

    @Override
    public boolean isEnabled() {
        return getEnvironment().getOptionBooleanValue(OPTION_TRACE);
    }

    @Override
    public void validate(Element element, ElementValidation validation) {
        validatorHelper.enclosingElementHasEnhancedComponentAnnotation(element, validation);

        validatorHelper.isNotPrivate(element, validation);

        validatorHelper.hasValidLogLevel(element, validation);
    }

    @Override
    public void process(Element element, EComponentHolder holder) throws Exception {
        ExecutableElement executableElement = (ExecutableElement) element;

        String tag = extractTag(executableElement);
        int level = executableElement.getAnnotation(Trace.class).level();

        JMethod method = codeModelHelper.overrideAnnotatedMethod(executableElement, holder);

        JBlock previousMethodBody = codeModelHelper.removeBody(method);

        JBlock methodBody = method.body();

        JInvocation isLoggableInvocation = getClasses().HI_LOG.staticInvoke("isLoggable");
        isLoggableInvocation.arg(DOMAIN).arg(tag).arg(logLevelFromInt(level, getClasses().HI_LOG));

        JConditional ifStatement = methodBody._if(isLoggableInvocation);

        JInvocation currentTimeInvoke = getClasses().SYSTEM.staticInvoke("currentTimeMillis");
        JBlock thenBody = ifStatement._then();

        // HI_LOG_LABEL
        IJExpression newHiLogLabel = _new(getClasses().HI_LOG_LABEL)
            .arg(getClasses().HI_LOG.staticRef("LOG_APP")).arg(DOMAIN).arg(tag);
        JVar hiLogLabel = thenBody.decl(getClasses().HI_LOG_LABEL,
            "hiLogLabel" + generationSuffix(), newHiLogLabel);

        // Log In
        String logMethodName = logMethodNameFromLevel(level);
        JInvocation logEnterInvoke = getClasses().HI_LOG.staticInvoke(logMethodName);
        logEnterInvoke.arg(hiLogLabel);

        logEnterInvoke.arg(getEnterMessage(method, executableElement));
        thenBody.add(logEnterInvoke);
        JVar startDeclaration = thenBody.decl(getCodeModel().LONG,
            "traceStart" + generationSuffix(), currentTimeInvoke);

        JTryBlock tryBlock;

        JVar result = null;
        if (method.type().fullName().equals("void")) {
            tryBlock = thenBody._try();
            tryBlock.body().add(previousMethodBody);
        } else {
            JInvocation superCall = codeModelHelper.getSuperCall(holder, method);
            result = thenBody.decl(getJClass(Object.class), "traceResult" + generationSuffix(), JExpr._null());
            tryBlock = thenBody._try();
            tryBlock.body().assign(result, superCall);
            tryBlock.body()._return(JExpr.cast(boxify(method.type()), result));
        }

        JBlock finallyBlock = tryBlock._finally();

        JVar durationDeclaration = finallyBlock.decl(getCodeModel().LONG,
            "traceDuration" + generationSuffix(), currentTimeInvoke.minus(startDeclaration));

        JInvocation logExitInvoke = getClasses().HI_LOG.staticInvoke(logMethodName);
        logExitInvoke.arg(hiLogLabel);

        logExitInvoke.arg(getExitMessage(executableElement, method, result, durationDeclaration));
        finallyBlock.add(logExitInvoke);

        JBlock elseBlock = ifStatement._else();

        elseBlock.add(previousMethodBody);
    }

    private AbstractJClass boxify(AbstractJType type) throws ClassNotFoundException {
        return getCodeModel().parseType(type.fullName()).boxify();
    }

    private IJExpression getExitMessage(ExecutableElement element,
                                        JMethod method, JVar result, JVar duration) throws ClassNotFoundException {
        String methodName = getMethodName(element);

        List<JVar> params = method.params();
        StringBuilder paramStr = new StringBuilder();
        for (int index = 0; index < params.size(); index++) {
            if (index > 0) {
                paramStr.append(", ");
            }
            JVar var = params.get(index);
            paramStr.append(var.type().name());
        }

        methodName += "(" + paramStr.toString() + ")";

        JInvocation format = getJClass(String.class).staticInvoke("format");
        if (result == null) {
            format.arg("Exiting [" + methodName + "], duration in ms: %d");
        } else {
            format.arg("Exiting [" + methodName + " returning: %s], duration in ms: %d");
            if (method.type().isArray()) {
                AbstractJClass arraysClass = getJClass(Arrays.class);
                format.arg(arraysClass.staticInvoke("toString").arg(JExpr.cast(boxify(method.type()), result)));
            } else {
                format.arg(result);
            }
        }

        return format.arg(duration);
    }

    private IJExpression getEnterMessage(JMethod method, ExecutableElement element) {
        String methodName = getMethodName(element);

        List<JVar> params = method.params();
        if (params.isEmpty()) {
            // early exit if the method has no parameters
            return JExpr.lit("Entering [" + methodName + "()]");
        }

        AbstractJClass arraysClass = getJClass(Arrays.class);
        StringBuilder paramStr = new StringBuilder();
        List<IJExpression> paramExpressions = new ArrayList<>();
        for (int index = 0; index < params.size(); index++) {
            if (index > 0) {
                paramStr.append(", ");
            }
            JVar var = params.get(index);
            paramStr.append(var.name()).append(" = %s");
            if (var.type().isArray()) {
                paramExpressions.add(arraysClass.staticInvoke("toString").arg(var));
            } else {
                paramExpressions.add(var);
            }
        }

        JInvocation format = getJClass(String.class).staticInvoke("format");
        format.arg(JExpr.lit("Entering [" + methodName + "(" + paramStr + ")]"));
        for (IJExpression expr : paramExpressions) {
            format.arg(expr);
        }

        return format;
    }

    private String getMethodName(ExecutableElement element) {
        String returnType = element.getReturnType().toString();
        String simpleName = element.getSimpleName().toString();
        return returnType + " " + simpleName;
    }

    private String logMethodNameFromLevel(int level) {
        switch (level) {
            case LOG_DEBUG:
                return "debug";
            case LOG_FATAL:
                return "fatal";
            case LOG_INFO:
                return "info";
            case LOG_WARN:
                return "warn";
            case LOG_ERROR:
                return "error";
            default:
                throw new IllegalArgumentException("Unrecognized Log level : " + level);
        }
    }

    private JFieldRef logLevelFromInt(int level, AbstractJClass logClass) {
        switch (level) {
            case LOG_DEBUG:
                return logClass.staticRef("DEBUG");
            case LOG_FATAL:
                return logClass.staticRef("FATAL");
            case LOG_INFO:
                return logClass.staticRef("INFO");
            case LOG_WARN:
                return logClass.staticRef("WARN");
            case LOG_ERROR:
                return logClass.staticRef("ERROR");
            default:
                throw new IllegalArgumentException("Unrecognized log level. Given value:" + level);
        }
    }

    private String extractTag(Element element) {
        Trace annotation = element.getAnnotation(Trace.class);
        String tag = annotation.tag();
        if (Trace.DEFAULT_TAG.equals(tag)) {
            tag = element.getEnclosingElement().getSimpleName().toString();
        }
        return trimLogTag(tag);
    }
}
